package de.mmis.core.publishsubscribe;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

import org.apache.log4j.Logger;

import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.publishsubscribe.filter.Filter;
import de.mmis.core.publishsubscribe.filter.Filter.FilterException;

/**
 * A thread-safe storage for {@link Filter}s and the corresponding clients.
 * 
 * @author Martin Nyolt
 * 
 * @param <C>
 *            The type of the client
 */
class FilterManager<C, I> {

	private final static Logger LOGGER = Logger.getLogger(FilterManager.class);

	static class FilterEntry<EC, EI> {
		private String clazz;
		private Filter filter;
		private EC client;
		private EI info;
		private Integer id;

		public String getClazz() {
			return clazz;
		}

		public Filter getFilter() {
			return filter;
		}

		public EC getClient() {
			return client;
		}

		public EI getInfo() {
			return info;
		}

		public Integer getID() {
			return id;
		}
	}

	private final HashMap<Integer, FilterEntry<C, I>> filter_map = new HashMap<Integer, FilterEntry<C, I>>();
	private final ReentrantReadWriteLock filters_rwl = new ReentrantReadWriteLock();
	private final ReadLock filters_rl = filters_rwl.readLock();
	private final WriteLock filters_wl = filters_rwl.writeLock();
	private final Collection<FilterEntry<C, I>> filters = filter_map.values();
	private final AtomicInteger lastID = new AtomicInteger(0);

	public Integer addFilter(String clazz, Filter filter, C client, I info) {
		Integer id = new Integer(lastID.incrementAndGet());

		FilterEntry<C, I> entry = new FilterEntry<C, I>();
		entry.clazz = clazz;
		entry.filter = filter;
		entry.client = client;
		entry.info = info;
		entry.id = id;

		filters_wl.lock();
		try {
			filter_map.put(id, entry);
		} finally {
			filters_wl.unlock();
		}

		return id;
	}

	/**
	 * Removes the filter with the id.
	 * 
	 * @return the {@link FilterEntry} associated with the id, or null if the id
	 *         was not found
	 */
	public FilterEntry<C, I> removeFilter(Integer id) {
		FilterEntry<C, I> entry;

		filters_wl.lock();
		try {
			entry = filter_map.remove(id);
		} finally {
			filters_wl.unlock();
		}

		return entry;
	}

	public LinkedList<FilterEntry<C, I>> removeClient(C client) {
		LinkedList<FilterEntry<C, I>> result = new LinkedList<FilterEntry<C, I>>();

		filters_wl.lock();
		try {
			Iterator<FilterEntry<C, I>> it = filters.iterator();
			while (it.hasNext()) {
				FilterEntry<C, I> entry = it.next();
				if (entry.client.equals(client)) {
					it.remove();
					result.add(entry);
				}
			}
		} finally {
			filters_wl.unlock();
		}

		return result;
	}

	public HashMap<C, LinkedList<FilterEntry<C, I>>> getForEvent(
			PlainEvent event, HashMap<String, Map<String, Tree>> worldstate) {
		HashMap<C, LinkedList<FilterEntry<C, I>>> result = new HashMap<C, LinkedList<FilterEntry<C, I>>>();

		filters_rl.lock();
		try {
			for (FilterEntry<C, I> entry : filters) {
				if (!event.classes.contains(entry.clazz))
					continue;

				Filter f = entry.filter;
				try {
					if (f == null || f.matches(event.eventData, worldstate)) {
						LinkedList<FilterEntry<C, I>> l = result
								.get(entry.client);

						if (l == null) {
							l = new LinkedList<FilterManager.FilterEntry<C, I>>();
							result.put(entry.getClient(), l);
						}

						l.add(entry);
					}
				} catch (FilterException e) {
					LOGGER.warn("Could not check if Filter matches event", e);
				}
			}
		} finally {
			filters_rl.unlock();
		}

		return result;
	}

	public LinkedList<FilterEntry<C, I>> getAllEntries() {
		LinkedList<FilterEntry<C, I>> result = new LinkedList<FilterManager.FilterEntry<C, I>>();

		filters_rl.lock();
		try {
			result.addAll(filters);
		} finally {
			filters_rl.unlock();
		}

		return result;
	}
}
